// --------------------------------------------------------------------------------------------------------------------
// <copyright file="DictionaryBuilderCommandGroup.cs" company="Open Trader">
//   Copyright (c) David Denis (david.denis@systemathics.com)
// </copyright>
// <summary>
//   |  Open Trader - The Open Source Systematic Trading Platform
//   |
//   |  This program is free software: you can redistribute it and/or modify
//   |  it under the terms of the GNU General Public License as published by
//   |  the Free Software Foundation, either version 2 of the License, or
//   |  (at your option) any later version.
//   |
//   |  This program is distributed in the hope that it will be useful,
//   |  but WITHOUT ANY WARRANTY; without even the implied warranty of
//   |  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//   |  GNU General Public License for more details.
//   |
//   |  You should have received a copy of the GNU General Public License
//   |  along with this program.  If not, see http://www.gnu.org/licenses
//   |
//   |  Up to date informations about Open Trader can be found at :
//   |    http://opentrader.org
//   |    http://opentrader.codeplex.com
//   |
//   |  For professional services, please visit us at :
//   |    http://www.systemathics.com
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Org.OpenTrader.Extensions.DictionaryBuilder
{
    #region Using Directives

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;

    using Org.OpenTrader.Framework;
    using Org.OpenTrader.Framework.CommandGroups;
    using Org.OpenTrader.Framework.Entities;
    using Org.OpenTrader.Framework.Forge.DataCache;
    using Org.OpenTrader.Framework.Forge.Entities;
    using Org.OpenTrader.Framework.Forge.Enums;
    using Org.OpenTrader.Framework.Forge.Interfaces;
    using Org.OpenTrader.Framework.LiveObjects;
    using Org.OpenTrader.Framework.LiveObjects.Interfaces;

    #endregion

    /// <summary>
    /// The execution context command group.
    /// </summary>
    public class DictionaryBuilderCommandGroup : CommandGroup
    {
        #region Constants and Fields

        /// <summary>
        /// The commands.
        /// </summary>
        private readonly IDictionary<string, Command> commands;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        /// Initializes a new instance of the <see cref="DictionaryBuilderCommandGroup"/> class.
        /// </summary>
        public DictionaryBuilderCommandGroup()
        {
            this.commands = new Dictionary<string, Command>();

            Command cmd;

            cmd = new BuildCommand(this);
            this.commands[cmd.Name] = cmd;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets Commands.
        /// </summary>
        public override IDictionary<string, Command> Commands
        {
            get
            {
                return this.commands;
            }
        }

        /// <summary>
        /// Gets Help.
        /// </summary>
        public override string Help
        {
            get
            {
                return "This command group is used to handle dictionary operations";
            }
        }

        /// <summary>
        /// Gets Name.
        /// </summary>
        public override string Name
        {
            get
            {
                return "dictionary_builder";
            }
        }

        #endregion

        /// <summary>
        /// The run command.
        /// </summary>
        public class BuildCommand : Command
        {
            #region Constants and Fields

            /// <summary>
            /// The identity.
            /// </summary>
            private static readonly Identity Identity = Identity.Create(System.Reflection.MethodBase.GetCurrentMethod());

            #endregion

            #region Constructors and Destructors

            /// <summary>
            /// Initializes a new instance of the <see cref="BuildCommand"/> class.
            /// </summary>
            /// <param name="commandGroup">
            /// The command group.
            /// </param>
            public BuildCommand(CommandGroup commandGroup)
                : base(commandGroup)
            {
            }

            #endregion

            #region Properties

            /// <summary>
            /// Gets Help.
            /// </summary>
            public override string Help
            {
                get
                {
                    return "build a dictionary with given parameters";
                }
            }

            /// <summary>
            /// Gets Name.
            /// </summary>
            public override string Name
            {
                get
                {
                    return "build";
                }
            }

            /// <summary>
            /// Gets Usage.
            /// </summary>
            public override string Usage
            {
                get
                {
                    return string.Format("{0} {1} execution_context_name configuration_file", this.CommandGroup.Name, this.Name);
                }
            }

            #endregion

            #region Public Methods

            /// <summary>
            /// The execute.
            /// </summary>
            /// <param name="args">
            /// The args.
            /// </param>
            /// <returns>
            /// </returns>
            public override Status Execute(string[] args)
            {
                if (args.Length < 2)
                {
                    return Status.Failure(Identity, "parameter execution_context_name or configuration_file is missing");
                }

                var contextName = args[0];
                var filePath = Environment.CurrentDirectory + @"\" + args[1];

                try
                {
                    // Load current context and get the root live object (application)
                    IApplication application;
                    DBExecutionContextHelpers.Load(contextName, out application);
                    if (application == null)
                    {
                        return Status.Failure(Identity, "Unable to load context " + contextName);
                    }

                    // The transient configuration schema for the dictionary
                    var dictionaryConfigurationSchema =
                        new ConfigurationSchema(
                            new Attribute[]
                                {
                                    new ConfigurationFieldAttribute("Markets", typeof(ConfigurationField.Table), "Markets subsection"), 
                                    new ConfigurationFieldAttribute("TickSizeRules", typeof(ConfigurationField.Table), "TickSizeRules subsection"), 
                                    new ConfigurationFieldAttribute("Instruments", typeof(ConfigurationField.Table), "Instruments subsection"),
                                    new ConfigurationFieldAttribute("CurrencyPrices", typeof(ConfigurationField.Table), "CurrencyPrices subsection")
                                });

                    // The transient configuration (using previously created schema) for the dictionary
                    var dictionaryConfiguration = new Configuration(Guid.NewGuid(), dictionaryConfigurationSchema, application["DictionaryService"].Container);

                    // The dictionary ID
                    var dictionaryID = application["DictionaryService"].Descriptor.ID;

                    // At this point we can point to the given configuration file an deserialize contents
                    var fileStream = File.OpenRead(filePath);
                    var streamReader = new StreamReader(fileStream);
                    var serializationContext = new SerializationContext(Environment.CurrentDirectory);

                    // Start desezialization
                    var status = dictionaryConfiguration.DeSerialize(streamReader, serializationContext);

                    // On success, use subsections to replenish the dictionary
                    if (status)
                    {
                        // Our dictionary service
                        var dbDictionaryService = DBDictionaryServiceHelpers.GetDBDictionaryService(dictionaryID);
                        if (dbDictionaryService == null)
                        {
                            DBDictionaryServiceHelpers.CreateDictionary(dictionaryID, ECurrency.EUR);
                            dbDictionaryService = DBDictionaryServiceHelpers.GetDBDictionaryService(dictionaryID);
                        }

                        // Start loading everything
                        if (!dbDictionaryService.DBMarket.IsLoaded)
                        {
                            dbDictionaryService.DBMarket.Load();
                        }

                        if (!dbDictionaryService.DBTickSizeRule.IsLoaded)
                        {
                            dbDictionaryService.DBTickSizeRule.Load();
                        }

                        if (!dbDictionaryService.DBCurrencyPrice.IsLoaded)
                        {
                            dbDictionaryService.DBCurrencyPrice.Load();
                        }
   
                        // First, setup markets
                        UpdateMarkets(dbDictionaryService, dictionaryConfiguration["Markets"]);

                        // Second, setup tick size rules
                        UpdateTSRs(dbDictionaryService, dictionaryConfiguration["TickSizeRules"]);

                        // Third, setup instruments
                        UpdateInstruments(dbDictionaryService, dictionaryConfiguration["Instruments"]);

                        // Fourth, setup currencies
                        UpdateCurrencyPrices(dbDictionaryService, dictionaryConfiguration["CurrencyPrices"]);

                        return Status.Success(Identity);
                    }

                    return Status.Failure(Identity, "Deserialization failed", status);
                }
                catch (Exception ex)
                {
                    return Status.Failure(Identity, "Got exception " + ex);
                }
                finally
                {
                    // Unload current context
                    DBExecutionContextHelpers.Unload();
                }
            }

            #endregion

            #region Methods

            /// <summary>
            /// The is valid db instrument.
            /// </summary>
            /// <param name="data">
            /// The data.
            /// </param>
            /// <returns>
            /// The is valid db instrument.
            /// </returns>
            private static bool IsValidDBInstrument(DBInstrumentStaticData data)
            {
                return data.Code != null && data.Name != null && data.ID != Guid.Empty && data.TradingScope != null && data.Type != null;
            }

            /// <summary>
            /// Setup a wiring, the wiring string has the following format :
            /// label;param1=value1;...;paramn=valuen
            /// Where label is the label of a wire (plugin) of the wanted type (IFeedPlugin for Feed Wiring, IOrderPlugin for Order Wiring)
            /// Where param1, ..., paramn are the fields of the related configuration schema
            /// </summary>
            /// <param name="wiringType">
            /// </param>
            /// <param name="parameters">
            /// </param>
            /// <param name="dbInstrument">
            /// </param>
            private static void SetupWiring(Type wiringType, string parameters, DBInstrumentStaticData dbInstrument)
            {
                var parametersList = new List<string>(parameters.Split(new[] { ';' }));
                if (parametersList.Count <= 0)
                {
                    return;
                }

                var label = parametersList[0];
                var wire = Singleton<LiveObjectsRegistry>.Instance.GetLiveObject(label);

                // The wire (plugin) wuth given label was not found. Stop.
                if (wire == null)
                {
                    Console.WriteLine(string.Format("No plugin with label \"{0}\" was found. Skipping wiring", label));
                    return;
                }

                // Create a Stream and context suitable for the DeSerialization of a configuration (used for wiring)
                var mm = new MemoryStream();
                var sw = new StreamWriter(mm) { AutoFlush = true };
                parametersList.RemoveAt(0);
                foreach (var assigment in parametersList)
                {
                    sw.WriteLine(assigment);
                }

                mm.Seek(0, SeekOrigin.Begin);

                // Here is the stream
                var sr = new StreamReader(mm);

                // Here is the memory context
                var ctx = new SerializationContext();

                // Feed Wiring and the label points to a feed plugin
                if (wiringType == typeof(IFeedWiring) && wire is IFeedPlugin)
                {
                    SetupWiringImpl("SubscriptionConfigurationSchema", "DBFeedPlugin", wire, dbInstrument, sr, ctx);
                    return;
                }

                // Order Wiring and the label points to a order plugin
                if (wiringType == typeof(IOrderWiring) && wire is IOrderPlugin)
                {
                    SetupWiringImpl("OrderRoutingConfigurationSchema", "DBOrderPlugin", wire, dbInstrument, sr, ctx);
                    return;
                }

                Console.WriteLine(string.Format("Wasn't able to setup wiring using plugin with label \"{0}\"", label));
            }

            /// <summary>
            /// The setup wiring impl.
            /// </summary>
            /// <param name="propertyName">
            /// The property name.
            /// </param>
            /// <param name="wiringName">
            /// The wiring Name.
            /// </param>
            /// <param name="wire">
            /// The wire.
            /// </param>
            /// <param name="dbInstrument">
            /// The db instrument.
            /// </param>
            /// <param name="sr">
            /// The sr.
            /// </param>
            /// <param name="ctx">
            /// The ctx.
            /// </param>
            private static void SetupWiringImpl(string propertyName, 
                                                string wiringName, 
                                                ILiveObject wire, 
                                                DBInstrumentStaticData dbInstrument, 
                                                StreamReader sr, 
                                                ISerializationContext ctx)
            {
                var schemaName = Reflection.GetProperty(wire, propertyName);
                var wiringConfigurationID = Reflection.GetProperty(dbInstrument, wiringName + "InstrumentStaticDataConfigurationID");

                IConfiguration configuration = null;
                if (wiringConfigurationID != null && (wiringConfigurationID as Guid?).HasValue)
                {
                    configuration = Singleton<DC>.Instance.LoadConfiguration(
                        (wiringConfigurationID as Guid?).Value, (wire.Descriptor as LiveObjectDescriptor.Local).ClassType, schemaName as string, wire.Container);
                }
                else
                {
                    var schema = wire.Container.GetConfigurationSchema((wire.Descriptor as LiveObjectDescriptor.Local).ClassType, schemaName as string);
                    if (schema != null)
                    {
                        configuration = new Configuration(Guid.NewGuid(), schema, wire.Container);
                    }
                }

                // Store old wiring
                var oldWiring = Reflection.GetProperty(dbInstrument, wiringName + "ID");

                // Set new wiring
                Reflection.SetProperty(dbInstrument, wiringName + "ID", wire.Descriptor.ID);

                // If a configuration is there, deserialize to update fields
                if (configuration != null)
                {
                    // deserialization
                    var status = configuration.DeSerialize(sr, ctx);
                    if (status)
                    {
                        // Deserialization ok

                        // This wiring has a configuration with this ID
                        Reflection.SetProperty(dbInstrument, wiringName + "InstrumentStaticDataConfigurationID", configuration.ID);

                        // Save updated configuration
                        Singleton<DC>.Instance.SaveConfiguration(configuration);
                    }
                    else
                    {
                        // configuration was not deserialized correctly, revert back to previous wiring
                        Reflection.SetProperty(dbInstrument, wiringName + "ID", oldWiring);
                    }
                }
            }

            /// <summary>
            /// The set value.
            /// </summary>
            /// <param name="method">
            /// The method.
            /// </param>
            /// <param name="rowIndex">
            /// The row index.
            /// </param>
            /// <param name="colName">
            /// The col name.
            /// </param>
            /// <param name="instance">
            /// The instance.
            /// </param>
            /// <param name="cell">
            /// The cell.
            /// </param>
            private static void SetValue(string method, int rowIndex, string colName, object instance, Table.Cell cell)
            {
                if (cell != null)
                {
                    bool valueChanged;
                    if (Reflection.SetProperty(instance, colName, cell, out valueChanged) && valueChanged)
                    {
                        //// Console.WriteLine("{0}: Property {1} value is now {2}", method, colName, cell.Content);
                    }
                    else
                    {
                        //// Console.WriteLine("{0}: Property {1} unchanged (setter not called)", method, colName);
                    }
                }
                else
                {
                    //// Console.WriteLine("{0}: Column {1} is empty at row {2}", method, colName, rowIndex);
                }
            }

            private static void UpdateCurrencyPrices(DBDictionaryService dbDictionary, Table currencyPricesTable )
            {
                if (currencyPricesTable == null)
                {
                    return;
                }

                for (var currencyCurrencyPriceRowIndex = 0; currencyCurrencyPriceRowIndex < currencyPricesTable.RowCount; currencyCurrencyPriceRowIndex++)
                {
                    try
                    {
                        var foreignCurrency = EnumDecoder<ECurrency>.Parse(currencyPricesTable[currencyCurrencyPriceRowIndex, "ForeignCurrency"]);
                        var foreignCurrencyPrice = (double)currencyPricesTable[currencyCurrencyPriceRowIndex, "ForeignCurrencyPrice"];

                        // If Foreign/Domestic is 1 that means that Foreign==Domestic, it's the domestic currency for the whole dictionary
                        if (foreignCurrencyPrice == 1)
                        {
                            dbDictionary.DomesticCurrency = foreignCurrency.ToString();
                        }

                        // Get required currency price
                        var dbCurrencyPrice = DBCurrencyPriceHelpers.GetDBCurrencyPrice(dbDictionary, foreignCurrency);

                        // Need creation ?
                        var create = dbCurrencyPrice == null;

                        Console.WriteLine("UpdateCurrencyPrices:   {0} CurrencyPrice {1}", create ? "Creating" : "Updating", foreignCurrency );

                        // New instance if none exists
                        if (create)
                        {
                            dbCurrencyPrice = new DBCurrencyPrice { ID = Guid.NewGuid() };
                        }

                        dbCurrencyPrice.ForeignCurrency = foreignCurrency.ToString();
                        dbCurrencyPrice.CurrencyPrice = foreignCurrencyPrice;

                        // If new one add to collection
                        if (create)
                        {
                            dbDictionary.DBCurrencyPrice.Add(dbCurrencyPrice);
                        }
                    }
                    catch
                    {
                    }
                }

                // Commit group per group
                var n = Singleton<DB>.Instance.Commit();
            }

            /// <summary>
            /// The update instruments.
            /// </summary>
            /// <param name="dbDictionary">
            /// The db dictionary.
            /// </param>
            /// <param name="instrumentsGroupTable">
            /// The instruments group table.
            /// </param>
            private static void UpdateInstruments(DBDictionaryService dbDictionary, Table instrumentsGroupTable)
            {
                if (instrumentsGroupTable == null)
                {
                    return;
                }

                for (var groupRowIndex = 0; groupRowIndex < instrumentsGroupTable.RowCount; groupRowIndex++)
                {
                    string group = instrumentsGroupTable[groupRowIndex, "Group"];
                    string type = instrumentsGroupTable[groupRowIndex, "Type"];
                    Table instrumentsTable = instrumentsGroupTable[groupRowIndex, "Table"];

                    Console.WriteLine("UpdateInstruments: Group {0} ({1})", group, type);

                    for (var currentInstrumentRowIndex = 0; currentInstrumentRowIndex < instrumentsTable.RowCount; currentInstrumentRowIndex++)
                    {
                        try
                        {
                            var code = (string)instrumentsTable[currentInstrumentRowIndex, "Code"];
                            var tradingScope = EnumDecoder<ETradingScope>.Parse(instrumentsTable[currentInstrumentRowIndex, "TradingScope"]);
                            var market = (string)instrumentsTable[currentInstrumentRowIndex, "Market"];
                            var tickSizeRule = (string)instrumentsTable[currentInstrumentRowIndex, "TickSizeRule"];
                            var feedwiring = (string)instrumentsTable[currentInstrumentRowIndex, "FeedWiring"];
                            var orderwiring = (string)instrumentsTable[currentInstrumentRowIndex, "OrderWiring"];

                            // Special fields : Market
                            var dbMarket = DBMarketHelpers.GetDBMarket(dbDictionary, market);
                            if (dbMarket == null)
                            {
                                Console.WriteLine(
                                    string.Format(
                                        "Warning: Skipping instrument {0} / {1} because the market path {2} wasn't found in database", 
                                        code, 
                                        tradingScope, 
                                        market));

                                // Skipping
                                continue;
                            }

                            if (!dbMarket.DBInstrumentStaticData.IsLoaded)
                            {
                                dbMarket.DBInstrumentStaticData.Load();
                            }

                            // Special fields : Tick Size Rule
                            var dbTickSizeRule = DBTickSizeRuleHelpers.GetDBTickSizeRule(dbDictionary, tickSizeRule);
                            if (dbTickSizeRule == null)
                            {
                                Console.WriteLine(
                                    string.Format(
                                        "Warning: Skipping instrument {0} / {1} because the tick size rule {2} wasn't found in database", 
                                        code, 
                                        tradingScope, 
                                        tickSizeRule));

                                // Skipping
                                continue;
                            }

                            // Lookup instrument
                            var dbInstrument = DBInstrumentStaticDataHelpers.GetDBInstrumentStaticData(dbMarket, code, tradingScope);

                            // Need creation ?
                            var create = dbInstrument == null;

                            Console.WriteLine("UpdateInstruments:   {0} Instrument {1} / {2}", create ? "Creating" : "Updating", code, tradingScope);

                            // New instance if none exists
                            if (create)
                            {
                                dbInstrument = new DBInstrumentStaticData { ID = Guid.NewGuid(), Type = type };
                            }

                            // Automatically try to update fields using reflection (property must thus have same name than column name in table)
                            foreach (var currentInstrumentColumnName in instrumentsTable.ColumnNames)
                            {
                                var cell = instrumentsTable[currentInstrumentRowIndex, currentInstrumentColumnName];
                                SetValue("UpdateIntruments", currentInstrumentRowIndex, currentInstrumentColumnName, dbInstrument, cell);
                            }

                            // Set the tick size rule
                            dbInstrument.DBTickSizeRuleID = dbTickSizeRule.ID;

                            // Special fields : FeedWiring
                            SetupWiring(typeof(IFeedWiring), feedwiring, dbInstrument);

                            // Special fields : OrderWiring
                            SetupWiring(typeof(IOrderWiring), orderwiring, dbInstrument);

                            // If new one add to collection
                            if (create)
                            {
                                if (IsValidDBInstrument(dbInstrument))
                                {
                                    dbMarket.DBInstrumentStaticData.Add(dbInstrument);
                                }
                                else
                                {
                                    Console.WriteLine(
                                        string.Format("Warning: Skipping instrument {0} / {1} because it didn't pass the validation", code, tradingScope));
                                }
                            }
                        }
                        catch
                        {
                        }
                    }
                }

                // Commit group per group
                var n = Singleton<DB>.Instance.Commit();
            }

            /// <summary>
            /// The update markets.
            /// </summary>
            /// <param name="dbDictionary">
            /// The db dictionary.
            /// </param>
            /// <param name="marketTable">
            /// The market table.
            /// </param>
            private static void UpdateMarkets(DBDictionaryService dbDictionary, Table marketTable)
            {
                if (marketTable == null)
                {
                    return;
                }

                for (var currentMarketRowIndex = 0; currentMarketRowIndex < marketTable.RowCount; currentMarketRowIndex++)
                {
                    try
                    {
                        // Market Path
                        var path = PathUtil.Cleanup(marketTable[currentMarketRowIndex, "Path"]);

                        // Lookup market
                        var dbMarket = DBMarketHelpers.GetDBMarket(dbDictionary, path);

                        // Need creation ?
                        var create = dbMarket == null;

                        // New instance if none exists
                        if (create)
                        {
                            dbMarket = new DBMarket { ID = Guid.NewGuid() };
                        }

                        // Automatically try to update fields using reflection (property must thus have same name than column name in table)
                        foreach (var currentMarketColumnName in marketTable.ColumnNames)
                        {
                            var cell = marketTable[currentMarketRowIndex, currentMarketColumnName];
                            SetValue("UpdateMarkets", currentMarketRowIndex, currentMarketColumnName, dbMarket, cell);
                        }

                        Console.WriteLine("UpdateMarkets: {0} Market {1}", create ? "Creating" : "Updating", dbMarket.Path);

                        // If new one add to collection
                        if (create)
                        {
                            dbDictionary.DBMarket.Add(dbMarket);
                        }
                    }
                    catch
                    {
                    }
                }

                var n = Singleton<DB>.Instance.Commit();
            }

            /// <summary>
            /// The update ts rs.
            /// </summary>
            /// <param name="dbDictionary">
            /// The db dictionary.
            /// </param>
            /// <param name="tsrListTable">
            /// The tsr list table.
            /// </param>
            private static void UpdateTSRs(DBDictionaryService dbDictionary, Table tsrListTable)
            {
                if (tsrListTable == null)
                {
                    return;
                }

                for (var groupRowIndex = 0; groupRowIndex < tsrListTable.RowCount; groupRowIndex++)
                {
                    // TSR Name
                    var name = (string)tsrListTable[groupRowIndex, "Name"];

                    // TSR Related table
                    Table tsrTable = tsrListTable[groupRowIndex, "Table"];

                    // Lookup instrument
                    var dbTickSizeRule = DBTickSizeRuleHelpers.GetDBTickSizeRule(dbDictionary, name);

                    // Need creation ?
                    var create = dbTickSizeRule == null;

                    // New instance if none exists
                    if (create)
                    {
                        dbTickSizeRule = new DBTickSizeRule { ID = Guid.NewGuid(), Name = name };
                    }

                    // Empty fields
                    foreach (var dbTickSizeRuleField in dbTickSizeRule.DBTickSizeRuleField.ToArray())
                    {
                        Singleton<DB>.Instance.Entities.DeleteObject(dbTickSizeRuleField);
                    }

                    dbTickSizeRule.DBTickSizeRuleField.Clear();

                    // Replace fields
                    for (var tsrRowIndex = 0; tsrRowIndex < tsrTable.RowCount; tsrRowIndex++)
                    {
                        try
                        {
                            var price = (double)tsrTable[tsrRowIndex, "Price"];
                            var tickSize = (double)tsrTable[tsrRowIndex, "TickSize"];
                            var field = new DBTickSizeRuleField { ID = Guid.NewGuid(), Price = price, TickSize = tickSize };
                            dbTickSizeRule.DBTickSizeRuleField.Add(field);
                        }
                        catch
                        {
                        }
                    }

                    Console.WriteLine("UpdateTSRs: {0} TSR {1}", create ? "Creating" : "Updating", dbTickSizeRule.Name);

                    // If new one add to collection
                    if (create)
                    {
                        dbDictionary.DBTickSizeRule.Add(dbTickSizeRule);
                    }
                }

                var n = Singleton<DB>.Instance.Commit();
            }

            #endregion
        }
    }
}